Conversation
Co-authored-by: Zack Eisbach <zack.eisbach@gmail.com>
…for vscode extension with default configurations.
…to not conflict with the webExtension configuration.
Co-authored-by: Jacob Lefkowitz <jacoblefk@gmail.com> Co-authored-by: ironmoon <me@ironmoon.dev>
Co-authored-by: ironmoon <me@ironmoon.dev>
Co-authored-by: ironmoon <me@ironmoon.dev>
With the goal of being able to support both file and in-memory cache.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR introduces initial Language Server Protocol (LSP) scaffolding for Pyret by adding a VS Code node extension entrypoint that starts a language client, plus a TypeScript LSP server that forwards textDocument/definition (jump-to-def) requests to the existing Pyret compiler server, backed by a new in-memory compilation cache.
Changes:
- Add a Node-targeted VS Code extension entry (
src/extension.ts) and webpack build config alongside the existing web extension. - Add a new
lsp/TypeScript server project (including a temporary node server implementation) and wire it into the VS Code extension viavscode-languageclient. - Refactor the Pyret compiler server protocol to support “info” requests and add an in-memory cache manager + LSP-side “jump-to-def” implementation.
Reviewed changes
Copilot reviewed 41 out of 48 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
vscode/webpack.config.js |
Adds a Node-target webpack build for the desktop extension entrypoint. |
vscode/src/webExtension.ts |
Minor formatting change; web extension activation remains. |
vscode/src/extension.ts |
New Node extension entrypoint that starts a language client to the TS LSP server. |
vscode/README.dev.md |
Updates dev instructions for testing web vs local desktop extension. |
vscode/package.json |
Adds main entrypoint + vscode-languageclient dependency and a new trace setting. |
vscode/package-lock.json |
Locks new dependency graph for vscode-languageclient. |
vscode/.vscode/launch.json |
Adds a debug launch configuration for running the extension. |
lsp/tsconfig.json |
TypeScript build config for the LSP server project. |
lsp/src/server-shared.ts |
Shared LSP server setup (initialize, documents, listen). |
lsp/src/server-node.ts |
Node LSP server entrypoint using vscode-languageserver/node. |
lsp/src/server-node-tmp.ts |
Temporary node server implementing jump-to-def via the Pyret compiler server over WS+unix. |
lsp/src/server-browser.ts |
Browser LSP server entrypoint using vscode-languageserver/browser. |
lsp/README.md |
Documents current build/debug hoops for hacking on the LSP. |
lsp/package.json |
Declares LSP server dependencies (vscode-languageserver, ws, etc.). |
lsp/package-lock.json |
Locks LSP server dependency graph. |
lsp/.gitignore |
Ignores node_modules and build output for the lsp/ package. |
lang/tsconfig.json |
Adds a strict editor-only TS config for checking JS via checkJs. |
lang/tests/type-check/main.arr |
Updates AST constructor calls to include locs. |
lang/tests/pyret/tests/test-letrec.arr |
Adds use context empty-context to test file. |
lang/tests/pyret/tests/test-compile-lib.arr |
Updates AST constructor calls to include locs. |
lang/src/types.d.ts |
Adds TS declaration file to improve editor IntelliSense for runtime/ABI. |
lang/src/js/trove/source-map-lib.js |
Wraps module object literal in parentheses; adds module typing hint. |
lang/src/arr/trove/ast.arr |
Changes Name variants (s-global, s-type-global, etc.) to include a Loc; updates visitors. |
lang/src/arr/compiler/type-structs.arr |
Adjusts pattern matches / constructors for updated Name shapes. |
lang/src/arr/compiler/type-defaults.arr |
Updates globals/type globals to pass locs when constructing names. |
lang/src/arr/compiler/type-check.arr |
Updates comparisons/pattern matches for new Name shapes. |
lang/src/arr/compiler/server.js |
Refactors server protocol handler to accept {command, options} and adds a safer run queue. |
lang/src/arr/compiler/server.arr |
Adds in-memory cache manager usage and an info command path for LSP requests. |
lang/src/arr/compiler/resolve-scope.arr |
Threads locs through make-atom/global name creation and pattern matches updated. |
lang/src/arr/compiler/lsp.arr |
New Pyret-side LSP helpers (currently jump-to-def via cached AST/env). |
lang/src/arr/compiler/desugar.arr |
Updates global name construction to include loc. |
lang/src/arr/compiler/desugar-post-tc.arr |
Updates global name construction to include loc. |
lang/src/arr/compiler/compile-structs.arr |
Adds lsp + cache-manager fields and placeholder to-info-json / print-static-info. |
lang/src/arr/compiler/compile-lib.arr |
Stores surface AST + named-result into the cache manager during compilation. |
lang/src/arr/compiler/cli-module-loader.arr |
Introduces cache-manager abstraction, adds in-memory cache, and threads cache-manager through module-finding/caching. |
lang/src/arr/compiler/ast-util.arr |
Updates name constructors and adds helpers for locating a name by srcloc and by cursor position. |
lang/src/arr/compiler/ast-anf.arr |
Updates make-atom calls for new loc parameter. |
lang/src/arr/compiler/anf.arr |
Updates make-atom calls for new loc parameter. |
lang/src/arr/compiler/anf-loop-compiler.arr |
Updates pattern matches and atom creation for new Name shapes. |
lang/package.json |
Adds @types/ws to dev dependencies. |
lang/package-lock.json |
Locks @types/ws addition and related metadata changes. |
docs/src/trove/ast.js.rkt |
Updates AST docs to reflect l member added to name variants. |
code.pyret.org/src/web/js/output-ui.js |
Updates comment about which AST nodes have l fields. |
code.pyret.org/src/web/js/ide.js |
Removes unused React import. |
code.pyret.org/package-lock.json |
Dependency metadata changes (peer flags / bundle reshuffles). |
.gitignore |
Ignores root /.vscode/ directory. |
.biome.json |
Adds Biome formatter config. |
Files not reviewed (4)
- code.pyret.org/package-lock.json: Language not supported
- lang/package-lock.json: Language not supported
- lsp/package-lock.json: Language not supported
- vscode/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)
lang/src/arr/compiler/cli-module-loader.arr:512
default-start-context/default-test-contextsetcache-managertoCS.default-compile-options.cache-manager, but that default cache-manager only providesset/get-surface-astandset/get-named-result(nocached-available,get-cached, etc.). Sincemodule-findernow callsctxt.cache-manager.cached-available(...)and friends, these contexts will throw at runtime (e.g. scripts/tests usingCLI.default-start-context). Usemake-file-cache()(or another full CacheManger implementation) for these defaults.
default-start-context = {
cache-manager: CS.default-compile-options.cache-manager,
current-load-path: Filesystem.resolve("./"),
cache-base-dir: Filesystem.resolve("./compiled"),
compiled-read-only-dirs: empty,
url-file-mode: CS.all-remote
}
default-test-context = {
cache-manager: CS.default-compile-options.cache-manager,
current-load-path: Filesystem.resolve("./"),
cache-base-dir: Filesystem.resolve("./tests/compiled"),
compiled-read-only-dirs: empty,
url-file-mode: CS.all-remote
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const child = childProcess.fork( | ||
| compilerPath, | ||
| ["-serve", "--port", portFile], | ||
| { | ||
| stdio: [0, 1, 2, "ipc"], | ||
| execArgv: ["-max-old-space-size=8192"], | ||
| }, |
There was a problem hiding this comment.
child_process.fork execArgv uses Node/V8 flags, which are typically --max-old-space-size=... (double hyphen). Using -max-old-space-size=8192 is not a recognized Node flag and may be ignored, defeating the intended memory limit increase.
| "runtimeExecutable": "${execPath}", | ||
| "args": ["--extensionDevelopmentPath=${workspaceRoot}"], | ||
| "outFiles": ["${workspaceFolder}/out/**/*.js"], | ||
| "preLaunchTask": "npm: compile" | ||
| } |
There was a problem hiding this comment.
The debug configuration’s outFiles points at ${workspaceFolder}/out/**/*.js, but this extension is built via webpack to dist/ (and the new node entrypoint outputs to dist/extension.js). With the current value, breakpoints/source-maps likely won’t bind. Update outFiles to match the emitted dist/**/*.js (or whatever the actual build output is).
| "files": [ | ||
| "dist/web/**/*", | ||
| "media/pyret-logo-128.png", | ||
| "language-configuration.json", | ||
| "syntaxes/pyret.tmLanguage.json" | ||
| ], | ||
| "browser": "./dist/web/extension.js", | ||
| "main": "./dist/extension.js", | ||
| "contributes": { |
There was a problem hiding this comment.
main now points to ./dist/extension.js, but the package’s files allowlist currently only includes dist/web/**/* (not dist/extension.js). This will make the node entrypoint missing from packaged installs. Add the node bundle (and its sourcemaps, if needed) to the files list or adjust the output location.
There was a problem hiding this comment.
TBH I have little idea... might be a Joe question??
There was a problem hiding this comment.
The more important thing is figuring out how to package and distribute for the web...
| ], | ||
| "description": "Controls how `url-file()` imports are resolved" | ||
| } | ||
| }, | ||
| "pyret.trace.server": { | ||
| "type": "string", | ||
| "enum": ["off", "messages", "verbose"], | ||
| "default": "verbose", | ||
| "scope": "resource", | ||
| "description": "Traces logs of the communication between VS Code and the language server." | ||
| } |
There was a problem hiding this comment.
A new setting pyret.trace.server is contributed here, but there’s no code wiring it up (e.g., calling client.setTrace(...) or configuring trace level based on this setting). As-is, changing the setting will have no effect. Either plumb it into LanguageClient initialization or remove it until it’s supported.
zackbach
left a comment
There was a problem hiding this comment.
Looking great! Mostly documentation nits, and a few opportunities for cleaner code.
| } | ||
|
|
||
| // NOTE(lsp): To add a new LSP feature: | ||
| // 1. Add a send*Request function below (following this pattern) |
There was a problem hiding this comment.
It seems likely that we can factor out some of the boilerplate here. I should be able to do this for hover.
| "files": [ | ||
| "dist/web/**/*", | ||
| "media/pyret-logo-128.png", | ||
| "language-configuration.json", | ||
| "syntaxes/pyret.tmLanguage.json" | ||
| ], | ||
| "browser": "./dist/web/extension.js", | ||
| "main": "./dist/extension.js", | ||
| "contributes": { |
There was a problem hiding this comment.
The more important thing is figuring out how to package and distribute for the web...
| ], | ||
| "description": "Controls how `url-file()` imports are resolved" | ||
| } | ||
| }, | ||
| "pyret.trace.server": { | ||
| "type": "string", | ||
| "enum": ["off", "messages", "verbose"], | ||
| "default": "verbose", | ||
| "scope": "resource", | ||
| "description": "Traces logs of the communication between VS Code and the language server." | ||
| } |
8ccb27c to
0bb800c
Compare
ironm00n
left a comment
There was a problem hiding this comment.
Ultra-Review: PR #51 — Initial LSP
+2197 / -575 across 46 files. Core changes: (1) add Loc to Name AST variants, (2) Pyret-side LSP query infrastructure, (3) TS LSP server + VS Code node extension.
CRITICAL (4)
C1. server.js:145 — Unguarded JSON.parse crashes the server on malformed WebSocket input
No try/catch around JSON.parse(message). A single bad message from any client takes down the entire Pyret compilation server.
C2. server.js:141 — message is a Buffer in ws 8.x, not string
The @type {string} JSDoc is wrong. ws 8.x delivers Buffer by default. Works by accident (Node's JSON.parse accepts Buffer), but any typeof check will fail.
C3. server-node-tmp.ts:57 — execArgv: ["-max-old-space-size=8192"] needs double dash
Node V8 flags require --max-old-space-size=8192. Single-dash is silently ignored, so the 8GB heap limit is not applied.
C4. types.d.ts:13-19 — pyret_pos missing endCol field
The runtime reads p.endCol (ffi.js:51) but the type has no endCol. Code using this type will produce undefined for endCol, causing NaN in source locations.
MAJOR (14)
M1. server.js:119-186 — No 'error' handler on WebSocket connections
In Node, an unhandled 'error' event crashes the process. A client disconnect (TCP reset) will emit an error event, taking down the server.
M2. server.js:58-87 — Queued items hold references to closed WebSocket connections
No connection.readyState check before connection.send(). If a client disconnects while its request is queued, send() throws.
M3. server-node-tmp.ts:150-206 — WebSocket connections leak (never closed)
sendJumpToDefRequest opens a new WebSocket per request but never calls client.close() after resolving/rejecting.
M4. server-node-tmp.ts:150-206 — No timeout on WebSocket requests
If the Pyret server hangs, the promise never settles and the LSP client freezes indefinitely.
M5. server-node-tmp.ts:176 — Unguarded JSON.parse in WebSocket message handler
Same issue as C1, but on the LSP server side. Malformed data from the Pyret server crashes the LSP process.
M6. server-node-tmp.ts:112-119 — Stale socket file causes silent failure
If a previous server crashed without cleanup, the socket file exists but no server is listening. The code skips startPyretServer and all subsequent requests fail. No liveness check.
M7. server.arr:140-145 — ask block with no otherwise fallback
Only "jump-to-def" is handled. An unknown query string throws an exception instead of returning a clean error. Same issue at lines 160-178 for serialization.
M8. query.arr — All failure paths return E.left([list:]) with no diagnostic info
Every none branch returns an empty error list. The caller logs torepr(errors) which is always "[list:]". Makes debugging impossible.
M9. compile-lib.arr:431 — Query path skips canonicalize-provides
When options.query is true, provides aren't canonicalized (unlike the normal path at line 439). Downstream code expecting module-uri(...) NameOrigins may break.
M10. extension.ts:22-24 — serverModule path resolves outside the extension directory
context.asAbsolutePath(path.join("..", "lsp", "out", "server-node-tmp.js")) escapes the extension root. Works in dev, breaks in any packaged/installed extension.
M11. vscode/package.json:18-23 — files excludes dist/extension.js
The files allowlist only has dist/web/**/*. The node entry point won't be in the packaged extension.
M12. types.d.ts:189 — PFunction.name typed as number, should be string
Runtime sets this.name = name || "anonymous". Constructor param is string but the property is declared number.
M13. types.d.ts:334 — makeOpqaue typo (should be makeOpaque)
Will never match the actual runtime export. Also missing the equals parameter.
M14. types.d.ts:265-268 — runThunk generic signature doesn't match runtime
Callback receives SuccessResult | FailureResult, not T. The type parameter U is declared but unused.
MINOR (14)
- m1.
server-node-tmp.ts:41-86—startPyretServerpromise can settle twice. No guard flag on"error"/"exit"handlers. - m2.
server-node-tmp.ts:66—child.unref()+disconnect()meansexithandler never fires.pyretServerProcessstays non-null forever. - m3.
cli-module-loader.arr:48-59—CacheManagertype incomplete. Missingget-cached-worklist,set-cached-worklist,invalidate. File-based cache doesn't implement these. - m4.
compile-structs.arr:2986-2991— Defaultcache-managerstub only has 4 methods. Any code hitting other methods crashes. - m5.
server.js:92-98— No error handler on HTTP serverlisten.process.send({type: 'success'})fires before the server is actually listening. - m6.
server.js:97— Double timestamp in log messages.makeLoggeralready prependsnew Date(). - m7.
vscode/.vscode/launch.json:10—outFilespoints toout/but webpack outputs todist/. Breakpoints won't bind. - m8.
vscode/package.json:126—pyret.trace.serverdefaults to"verbose". Will flood the output channel for all users. - m9.
lsp/package.json:14—@types/wsindependenciesinstead ofdevDependencies. - m10.
lsp/package.json:7—main: "index.js"points to nonexistent file. - m11.
ast-util.arr:1551-1601—find-name-key-by-srclocmissings-type-global/s-module-globalhandlers. Jump-to-def silently fails for type names and module names. - m12.
extension.ts:58—client.start()not awaited/caught. Unhandled rejection if server fails to start. - m13.
types.d.ts:347-364—check*functions acceptunknown, not the specific JS types declared. These are runtime assertion functions on Pyret values. - m14.
types.d.ts:180—POpaquemissingvalandequalsfields.
NITS (6)
ast.arr— Multiple# ZACK:comments should be cleaned up or formalized.server.arr:43-46— Commented-outprintstatements.server-node-tmp.ts— File named-tmpbut is the actual entry point.server-node.ts— Dead code (nothing references it).lsp/package.json:5—"license": "UNLICENSED"on an open-source repo.server.js:169— Stray semicolon after case block brace.
Pre-existing bugs found (not blocking, but worth tracking)
ast-util.arr:1106—t-ref(ann-to-typ(ann), false)missinglargument (needs 3 args).ast-util.arr:271—s-methodreconstructed with wrong arity (dropsnameandblocky).
Name-carries-Loc consistency: PASS
All pattern matches on s-global, s-module-global, s-type-global, s-atom across resolve-scope, type-check, type-structs, type-defaults, desugar, desugar-post-tc, anf, anf-loop-compiler, and ast-anf have been updated correctly. Name._equals compares by .key() only (excludes loc), so == comparisons with dummy-loc are safe.
Top 5 things to fix before merge
- C1+M1 — Add error handling to
server.js(try/catchJSON.parse, addwserror handlers). A single bad message shouldn't kill the compilation server. - C3 — Double-dash on
--max-old-space-size=8192. - M3+M4 — Close WebSocket connections after use and add a timeout in
sendJumpToDefRequest. - M8 — Add diagnostic strings to
query.arrfailure paths so debugging is possible. - M10+M11 — Fix extension packaging (
filesallowlist,serverModulepath) before it bites someone.
Generated by Claude Code (claude-opus-4-6)
fixes: #40
fixes: #17
fixes: #18